package de.hs_mannheim.visualscheduler.scheduling;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

/**
 * The Class AbstractScheduler is the base-class for all schedulers. It hides not yet started or completed
 * Processes from the Scheduler implementation. It also handles preemptive algorithms by interrupting the scheduling
 * at the point of arrival of another process, if necessary. Additionally the process-list is sorted by arrival.
 */
public abstract class AbstractScheduler implements Scheduler {
	
	/**
	 * The Class Process combines a Process Definition with a time-left variable.
	 */
	protected static final class Process {
		protected final ProcessDefinition	definition;
		private int							timeleft;

		Process(final ProcessDefinition definition) {
			this.definition = definition;
			setTimeleft(definition.getRuntime());
		}

		/**
		 * Gets the time needed for the Process to complete.
		 * 
		 * @return the timeleft
		 */
		public int getTimeleft() {
			return timeleft;
		}

		void setTimeleft(final int timeleft) {
			this.timeleft = timeleft;
		}
	}

	/**
	 * The Class ScheduleResult is a transfer object for communicating
	 * the scheduler implementations result to the AbstractScheduler.
	 */
	protected static final class ScheduleResult {
		
		/** The process to work on. */
		protected final Process	processToWorkOn;
		/** The time to work for. */
		int		timeToWorkFor;

		/**
		 * Instantiates a new schedule result.
		 * 
		 * @param processToWorkOn
		 *            the process to work on
		 * @param timeToWorkFor
		 *            the time to work for
		 */
		protected ScheduleResult(final Process processToWorkOn, final int timeToWorkFor) {
			this.processToWorkOn = processToWorkOn;
			this.timeToWorkFor = timeToWorkFor;
		}
	}

	@Override
	public SchedulingResult performScheduling(final ProcessDefinition[] processDefinitions, int timeSliceLength) {
		prepareForScheduling(timeSliceLength);

		// Initialize the Process' states
		final ProcessState[] jobStates = new ProcessState[processDefinitions.length];
		for (int i = 0; i < jobStates.length; i++) {
			jobStates[i] = ProcessState.NOT_ARRIVE_YET;
		}

		int jobsPending = processDefinitions.length;
		final ArrayList<Process> processes = new ArrayList<Process>();
		final SchedulingResult result = new SchedulingResult();
		int time = 0;
		int added = 0;
		while (jobsPending > 0) { // Still processes left
			// Append newly arrived Jobs to the end of the list
			for (int i = 0; i < processDefinitions.length; i++) {
				if (jobStates[i] == ProcessState.NOT_ARRIVE_YET && processDefinitions[i].getArrival() <= time) {
					// Start this process
					jobStates[i] = ProcessState.WAITING_FOR_START;
					processes.add(new Process(processDefinitions[i]));
					added++;
	
					// If this process arrived during the last work-cycle,
					// break the last result apart at the point of arrival of this Process
					if (processDefinitions[i].getArrival() < time) {
						int last = result.size();
						while (--last >= 0) {
							if (result.get(last).getStartTime() < processDefinitions[i].getArrival()) {
								final Timeblock cloned = new Timeblock(result.get(last));
								final int duration = result.get(last).getDuration();
								result.get(last).setDuration(
										processDefinitions[i].getArrival() - result.get(last).getStartTime());
								cloned.setStartTime(processDefinitions[i].getArrival());
								cloned.setDuration(duration - result.get(last).getDuration());
								cloned.put(processDefinitions[i], ProcessState.WAITING_FOR_START);
								result.add(last + 1, cloned);
								break;
							}
							result.get(last).put(processDefinitions[i], ProcessState.WAITING_FOR_START);
						}
					}
				}
			}
			if (added > 1) { // If more than one Job is added the list must be re-sorted
				final Process[] toSort = processes.toArray(new Process[processes.size()]);
				Arrays.sort(toSort, new Comparator<Process>() {
					@Override
					public int compare(final Process o1, final Process o2) {
						return o1.definition.getArrival() - o2.definition.getArrival();
					}
				});
				processes.clear();
				for (final Process p : toSort) {
					processes.add(p);
				}
			}

			// Skip 'nothing to do' situations
			if (processes.size() == 0) {
				time++;
				continue;
			}

			// Delegate to the implementations
			final ScheduleResult singleResult = performSchedulingBlock(processes);
			
			// Verify that the Scheduler-Implementation works correctly
			// TODO Could be removed in a final release where all Schedulers work properly
			if (!processes.contains(singleResult.processToWorkOn))
				throw new IllegalArgumentException("Invalid Process");
			if (singleResult.timeToWorkFor > singleResult.processToWorkOn.getTimeleft())
				throw new IllegalArgumentException("To much time");

			// Handle Preemptive algorithms
			if(isPreemptive()){
				for (int i = 0; i < processDefinitions.length; i++) {
					if (jobStates[i] == ProcessState.NOT_ARRIVE_YET && processDefinitions[i].getArrival() <= time + singleResult.timeToWorkFor) {
						singleResult.timeToWorkFor = processDefinitions[i].getArrival() - time;
					}
				}
			}
			
			// Update the process that was worked on
			singleResult.processToWorkOn.setTimeleft(singleResult.processToWorkOn.getTimeleft()
					- singleResult.timeToWorkFor);

			// Keep track of the process-states
			final Timeblock partialResult = new Timeblock(time, singleResult.timeToWorkFor);
			partialResult.put(singleResult.processToWorkOn.definition, ProcessState.STARTED_AND_RUNNING);
			for (int i = 0; i < processDefinitions.length; i++) {
				if (singleResult.processToWorkOn.definition == processDefinitions[i]) {
					// Remove complete process
					if (singleResult.processToWorkOn.getTimeleft() == 0) {
						processes.remove(singleResult.processToWorkOn);
						jobsPending--;
						jobStates[i] = ProcessState.COMPLETED;
					} else {
						jobStates[i] = ProcessState.STARTED_AND_WAITING;
					}
				} else {
					partialResult.put(processDefinitions[i], jobStates[i]);
				}
			}

			// Add this action to the result
			result.add(partialResult);

			time += singleResult.timeToWorkFor;
		}
		result.update();
		return result;
	}

	/**
	 * Checks if is preemptive.
	 * 
	 * @return true, if is preemptive
	 */
	protected abstract boolean isPreemptive();
	
	@Override
	public String toString() { // JComboBox calls this for displaying
		return getAlgorithmName();
	}

	/**
	 * Gets the algorithm name.
	 * 
	 * @return the algorithm name
	 */
	protected abstract String getAlgorithmName();

	/**
	 * Perform scheduling block. Decide which process to work on next and for how long.
	 * 
	 * @param active
	 *            a list of all currently available processes sorted ascending by arrival.
	 * @return the schedule result
	 */
	protected abstract ScheduleResult performSchedulingBlock(ArrayList<Process> active);

	/**
	 * Prepare for scheduling. Reset internal buffers here.
	 * 
	 * @param timeSliceLength
	 *            the time slice length to use from now on
	 */
	protected abstract void prepareForScheduling(int timeSliceLength);
}
